package nl.ru.rd.facedetection.nnbfd.neuralnetwork;

/**
 * A backpropagatable metalayer.
 * 
 * This Layer binds the Nodes of multiple Layers to the Nodes of this Layer. Its backpropagatable to boot.
 * 
 * @author Wouter Geraedts (s0814857)
 */
public class BMLayer implements Backpropagatable
{
	private static final long serialVersionUID = -1462382961407573505L;
	private Layer[] previousLayers;
	private int previousTotalSize;

	private int size;

	private Activationfunction f;

	private double[][] weights;
	private double[] activations;

	private Layer nextLayer;

	private double[] errorDeltas;

	/**
	 * Create a backpropagatable metalayer.
	 * 
	 * @param previousLayers
	 *            The previous Layers to be bound to.
	 * @param size
	 *            The number of Nodes in this Layer.
	 * @param f
	 *            The activationfunction to be used by Nodes of this Layer.
	 */
	public BMLayer(Layer[] previousLayers, int size, Activationfunction f)
	{
		this.previousLayers = previousLayers;
		this.size = size;
		this.f = f;

		this.previousTotalSize = 0;
		for(int i = 0; i < this.previousLayers.length; i++)
		{
			this.previousTotalSize += this.previousLayers[i].getSize();
			this.previousLayers[i].bind(this);
		}

		this.weights = new double[this.size][this.previousTotalSize];
		this.activations = new double[this.size];
		this.errorDeltas = new double[this.size];

		for(int i = 0; i < size; i++)
			for(int j = 0; j < this.previousTotalSize; j++)
				this.weights[i][j] = Math.random() * 2.0 - 1.0;
	}

	private int findLayerIndex(Layer layer)
	{
		for(int i = 0; i < this.previousLayers.length; i++)
			if(this.previousLayers[i].equals(layer))
				return i;

		return -1;
	}

	private int findOffset(int layerIndex)
	{
		int offset = 0;
		for(int i = 0; i < layerIndex; i++)
			offset += this.previousLayers[i].getSize();

		return offset;
	}

	/**
	 * Gets the weight for a specific synapse.
	 * 
	 * @param i
	 *            The index of the neuron in this layer.
	 * @param j
	 *            The index of the neuron in the previousLayer.
	 * @return The weight of a specific synapse.
	 * 
	 * @see NLayer#getWeight(int, int)
	 */
	protected double getWeight(int i, int j)
	{
		return this.weights[i][j];
	}

	/**
	 * Sets the weight for a specific synapse.
	 * 
	 * @param i
	 *            The index of the neuron in this layer.
	 * @param j
	 *            The index of the neuron in the previousLayer.
	 * @param value
	 *            The new weight for the specific synapse.
	 * 
	 * @see NLayer#setWeight(int, int, double)
	 */
	protected void setWeight(int i, int j, double value)
	{
		this.weights[i][j] = value;
	}

	public double calculateErrorSum(Layer layer, int previousLayerNodeId)
	{
		previousLayerNodeId += this.findOffset(this.findLayerIndex(layer));

		double error = 0;
		for(int i = 0; i < this.getSize(); i++)
			error += this.errorDeltas[i] * this.getWeight(i, previousLayerNodeId);

		return error;
	}

	public void updateDeltas(double[] targets)
	{
		for(int i = 0; i < this.getSize(); i++)
		{
			double activation = this.getActivation(i);
			double error = targets[i] - activation;
			this.errorDeltas[i] = error * this.f.calculateDerivative(activation);
		}
	}

	public void updateDeltas()
	{
		Backpropagatable layer = (Backpropagatable) this.getNextLayer();

		for(int i = 0; i < this.getSize(); i++)
		{
			double error = layer.calculateErrorSum(this, i);
			this.errorDeltas[i] = error * this.f.calculateDerivative(this.getActivation(i));
		}
	}

	public void updateWeights(double learningRate)
	{
		for(int i = 0; i < this.getSize(); i++)
		{
			int j = 0;
			for(int layerIndex = 0; layerIndex < this.previousLayers.length; layerIndex++)
			{
				Layer currentLayer = this.previousLayers[layerIndex];
				for(int nodeIndex = 0; nodeIndex < currentLayer.getSize(); nodeIndex++, j++)
				{
					double change = this.errorDeltas[i] * currentLayer.getActivation(nodeIndex);
					this.setWeight(i, j, this.getWeight(i, j) + learningRate * change);
				}
			}
		}
	}

	public void bind(Layer layer)
	{
		this.nextLayer = layer;
	}

	public double getActivation(int i)
	{
		return this.activations[i];
	}

	public Layer getNextLayer()
	{
		return this.nextLayer;
	}

	public int getSize()
	{
		return this.size;
	}

	public void update()
	{
		for(int i = 0; i < this.size; i++)
		{
			double sum = 0.0;

			int j = 0;
			for(int layerIndex = 0; layerIndex < this.previousLayers.length; layerIndex++)
			{
				Layer currentLayer = this.previousLayers[layerIndex];
				for(int nodeIndex = 0; nodeIndex < currentLayer.getSize(); nodeIndex++, j++)
					sum += this.weights[i][j] * currentLayer.getActivation(nodeIndex);
			}

			this.activations[i] = this.f.calculate(sum);
		}
	}
}
